class_name GameplayEffectManager
extends RefCounted

## 效果管理器的持有者
var owner: Variant

## 按施加者堆叠的效果
## { effect_name-String: { source-Variant: effect-GameplayEffect } }
var effect_map_by_source: Dictionary

## 按持有者堆叠的效果
## { effect_name-String: effect-GameplayEffect }
var effect_map_by_target: Dictionary

## 按照所修改的属性存储效果，当移除效果时，重新计算对应的属性
## { attribute_name-String: [ effect-GameplayEffect, ... ] }
var affected_attribute_map: Dictionary


func _init(init_owner: Variant) -> void:
	owner = init_owner
	effect_map_by_source = {}
	effect_map_by_target = {}
	affected_attribute_map = {}


func attach_effect(effect_asset: GameplayEffectAsset, source: Variant) -> void:
	if owner.tag_manager.has_any_tag(effect_asset.tags_cancelled_effect):
		return
	
	# 当重复施加效果且未选择叠加策略时，忽略该效果
	if effect_asset.stack_policy == GameplayEffectAsset.StackPolicy.None and\
	   (effect_map_by_source.has(effect_asset.name) or effect_map_by_target.has(effect_asset.name)):
		return

	
	var is_instant: bool = not effect_asset.has_component("DurationEffectComponent")

	if is_instant:
		effect_asset.create_spec(owner).apply_effect()
	else:
		var effect: GameplayEffect

		match effect_asset.stack_policy:
			GameplayEffectAsset.StackPolicy.AggregateBySource:
				effect = effect_map_by_source.get_or_add(effect_asset.name, {}).get_or_add(source, effect_asset.create_spec(owner))
			GameplayEffectAsset.StackPolicy.None, GameplayEffectAsset.StackPolicy.AggregateByTarget:
				effect = effect_map_by_target.get_or_add(effect_asset.name, effect_asset.create_spec(owner))
			_:
				Logger.error("Invalid stack policy.")
				return
			
		var affected_attribute_effect_cache = affected_attribute_map.get_or_add(effect_asset.attribute_name, [])
		if not affected_attribute_effect_cache.has(effect):
			affected_attribute_map[effect_asset.attribute_name].append(effect)

		effect.on_attach(source)
		_on_effect_changed(effect_asset.attribute_name)


func attach_effects(effect_assets: Array[GameplayEffectAsset], source: Variant) -> void:
	for effect_asset: GameplayEffectAsset in effect_assets:
		attach_effect(effect_asset, source)


func detach_effect(effect_asset: GameplayEffectAsset, source: Variant) -> void:
	var effect: GameplayEffect
	var effect_map: Dictionary

	match effect_asset.stack_policy:
		GameplayEffectAsset.StackPolicy.AggregateBySource:
			effect = effect_map_by_source.get_or_add(effect_asset.name, {}).get(source)
			effect_map = effect_map_by_source
		GameplayEffectAsset.StackPolicy.None, GameplayEffectAsset.StackPolicy.AggregateByTarget:
			effect = effect_map_by_target.get(effect_asset.name)
			effect_map = effect_map_by_target
		_:
			Logger.error("Invalid stack policy.")
			return

	if effect == null:
		Logger.warning("No such effect with given effect asset '%s'." % effect_asset.name)
		return
	
	effect.on_detach(source)

	if effect.sources.size() == 0:
		effect_map.erase(effect_asset.name)
		affected_attribute_map.get(effect_asset.attribute_name, []).erase(effect)
		_on_effect_changed(effect_asset.attribute_name)


func detach_effects(effect_assets: Array[GameplayEffectAsset], source: Variant) -> void:
	for effect_asset: GameplayEffectAsset in effect_assets:
		detach_effect(effect_asset, source)


func on_update(delta: float) -> void:
	var expired_effects: Array[GameplayEffect] = []

	for effect_cache in effect_map_by_source.values():
		for effect: GameplayEffect in effect_cache.values():
			if effect.expired:
				expired_effects.append(effect)
			else:
				if effect.dirty:
					_on_effect_changed(effect.effect_asset.attribute_name)
					effect.dirty = false
				effect.on_update(delta)

	for effect: GameplayEffect in expired_effects:
		detach_effect(effect.effect_asset, effect.sources[0])
	
	expired_effects = []

	for effect: GameplayEffect in effect_map_by_target.values():
		if effect.expired:
			expired_effects.append(effect)
		else:
			if effect.dirty:
				_on_effect_changed(effect.effect_asset.attribute_name)
				effect.dirty = false
			effect.on_update(delta)

	for effect: GameplayEffect in expired_effects:
		for source: Variant in effect.sources:
			detach_effect(effect.effect_asset, source)


func _on_effect_changed(affected_attribute_name: String) -> void:
	var attribute: Attribute = owner.attribute_manager.get_attribute(affected_attribute_name)
	attribute.reset()
	
	for effect: GameplayEffect in affected_attribute_map.get(affected_attribute_name, []):
		if not effect.effect_asset.has_component("PeriodEffectComponent"):
			effect.apply_effect()
